# Author: Ryan Myers
# Models: Jeff Styers, Reagan Heller

# Last Updated: 6/13/2005
#
# This tutorial provides an example of creating a character
# and having it walk around on uneven terrain, as well
# as implementing a fully rotatable camera.

import direct.directbase.DirectStart
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionSphere,CollisionRay
from panda3d.core import Filename,AmbientLight,DirectionalLight
from panda3d.core import PandaNode,NodePath,Camera,TextNode
from panda3d.core import Vec3,Vec4,BitMask32
from pandac.PandaModules import MovieTexture
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
import random, sys, os, math, time

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
                        pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

# Function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                        pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)

class World(DirectObject):

    def setInst(self, idx, msg):
        if idx >= len(self.instLoc):
            print "instLoc OOB: {0}".format(idx)
            return
        if idx >= len(self.instructions):
            print "inst OOB: {0}".format(idx)
            return
        if self.instructions[idx] is not None:
            self.instructions[idx].destroy()
        self.instructions[idx] = addInstructions(self.instLoc[idx], msg)

    def __init__(self):
        
        self.keyMap = {"left":0, "right":0, "forward":0, "backward":0, "turned":0, "running":0, "jumping":0, "grounded":0}
        base.win.setClearColor(Vec4(0,0,0,1))

        # Post the instructions

        #self.title = addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)")
        self.instLoc = [0.90, 0.85, 0.80, 0.75, 0.70, 0.65]
        self.instructions = [None, None, None, None, None, None]
        self.setInst(0, "[GAME STATE]")
        self.setInst(1, "[UPSPEED]")
        self.setInst(2, "[JUMPING?]")
        self.setInst(3, "[CAM AZ ZEN]")
        self.setInst(4, "[CAM X Y Z]")
        self.setInst(5, "Camera: first person")
        
        # Set up the environment
        #
        # This environment model contains collision meshes.  If you look
        # in the egg file, you will see the following:
        #
        #    <Collide> { Polyset keep descend }
        #
        # This tag causes the following mesh to be converted to a collision
        # mesh -- a mesh which is optimized for collision, not rendering.
        # It also keeps the original mesh, so there are now two copies ---
        # one optimized for rendering, one for collisions.  

        # LEVEL 1-1
        self.environ = loader.loadModel("../models/ground/level_1-1")
        # LEVEL 1-2
        #self.environ = loader.loadModel("../models/one-two/level_1-2")
        self.environ.reparentTo(render)
        self.environ.setPos(0,0,0)
        
        # Create the main character, Ralph

        #ralphStartPos = self.environ.find("**/start_point").getPos()
        self.ralph = Actor("models/ralph",
                                 {"run":"models/ralph-run",
                                  "walk":"models/ralph-walk"})
        self.ralph.reparentTo(render)
        self.ralph.setScale(.5)
        self.ralph.setPos(0, 5, 6)
        self.ralph.setHpr(180.0, 0, 0)

        #-------------
        # Init Clipping Plane
        #------------
        
        self.initClippingPlane()
        
        # Create a floater object.  We use the "floater" as a temporary
        # variable in a variety of calculations.
        
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(render)

        # Accept the control keys for movement and rotation

        self.accept("escape", sys.exit)
        self.accept("a", self.setKey, ["running",1])
        self.accept("mouse1", self.setKey, ["turned",1])
        self.accept("s", self.setKey, ["backward",1])
        self.accept("w", self.setKey, ["forward",1])
        self.accept("c", self.toggleCameraMode)
        self.accept("v", self.toggleViewCollisions)
        self.accept("space", self.setKey, ["jumping",1])
        self.accept("a-up", self.setKey, ["running",0])
        self.accept("w-up", self.setKey, ["forward",0])
        self.accept("mouse1-up", self.setKey, ["turned",0])
        self.accept("s-up", self.setKey, ["backward",0])
        self.accept("m", self.soundTester)

        taskMgr.add(self.move,"moveTask")
        taskMgr.add(self.cameraTask,"cameraTask")

        # ----------
        # Init Music
        # ----------
        self.jumpSound = base.loadMusic("../sounds/effects/mario_jump.wav")
        self.coinSound = base.loadMusic("../sounds/effects/coin.wav")
        self.breakBlockSound = base.loadMusic("../sounds/effects/smb_breakblock.wav")
        self.fireballSound = base.loadMusic("../sounds/effects/fireball.wav")
        self.deathSound = base.loadMusic("../sounds/effects/death.wav")
        self.gameoverSound = base.loadMusic("../sounds/effects/game_over.wav")
        self.winStageSound = base.loadMusic("../sounds/effects/win_stage.wav")

        # -------------
        # Text Messages
        # -------------
        self.deathText = OnscreenText(text="GAME OVER", style=3, fg=(1,0,0,1), pos=(0.0, 0.0), align=TextNode.ACenter, scale = .25, mayChange=True)
        self.deathText.hide(BitMask32.allOn())
        self.deathText2 = OnscreenText(text="Press ESC to exit", style=3, fg=(1,0,0,1), pos=(0.0, -0.1), align=TextNode.ACenter, scale = .05, mayChange=True)
        self.deathText2.hide(BitMask32.allOn())

        # Game state variables
        self.isMoving = False

        # For determining time between frames
        self.startTime = 0
        self.lastTime = globalClock.getFrameTime()
        self.dTime = 0

        # movement speed
        self.WALKSPEED = 25
        self.RUNSPEED = 50

        # For jumping / falling
        self.GRAVITY = 1.5
        self.JUMPVECTOR = 20
        self.MAXDOWNSPEED = -100
        self.upSpeed = 0
        self.downForce = 0
        self.ZBOUND = -15

        # Enemies
        self.enemies = {} # a record is: {int: ( eggmodel, direction )}, where direction = -1 for "toward start"
        self.goombaSX = .25
        self.goombaSY = .25
        self.goombaSZ = .25

        #Load the Goombae
        self.enemies[0] = (loader.loadModel("../models/goomba/goomba"), -1)
        enemy = self.enemies[0][0]
        enemy.reparentTo(render)
        enemy.setPos(0, 32, 1)
        enemy.setScale(self.goombaSX, self.goombaSY, self.goombaSZ)

        # define each enemy's movement
        for key, enemyRecord in self.enemies.items() :
            #define first interval
            Sequence(LerpPosInterval(enemyRecord[0], 1, self.goombaTarget(enemyRecord, enemyRecord[0].getPos())),
                     Func(self.nextGoombaMove, enemyRecord, self.goombaTarget(enemyRecord, enemyRecord[0].getPos()))).start()

        # Set up the camera
        base.disableMouse()
        base.camera.setPos(self.ralph.getX(),self.ralph.getY(),self.ralph.getZ()+1)
        self.ralph.setHpr(self.ralph.getH(), self.ralph.getP(), self.ralph.getR())

        # how high the camera floats above the ground
        self.CAMERA_Z_OFFSET = 0 # self.CAMZ
        self.CAMDISTANCE = 30 # distance for 3rd person
        self.YROTCONST = 30.0
        self.XROTCONST = -30.0
        self.CAM_ZEN_SCALE = math.pi / 8.0
        self.CAM_ZEN_OFFSET = - 3.0 * math.pi / 8.0
        self.CAM_AZ_SCALE = math.pi / 8.0
        self.CAM_AZ_OFFSET = 5.0 * math.pi / 8.0

        # 1 = first person, 2 = third person
        self.CAMSTYLE = 1

        # view collisions?
        self.VIEWCOL = 0
        
        # We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain.  One ray will
        # start above ralph's head, and the other will start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.

        self.cTrav = CollisionTraverser()

        # Ground level sphere for detecting object collisions
        self.ralphGroundSphereHandler = CollisionHandlerQueue()
        self.ralphGroundSphereColNp = self.setupSphereCollider(self.ralph, 0, 0, 1.5, 1, 'ralphGroundSphere', self.ralphGroundSphereHandler, True)

        # Head level sphere for detecting object collisions
        self.ralphHeadSphereHandler = CollisionHandlerQueue()
        self.ralphHeadSphereColNp = self.setupSphereCollider(self.ralph, 0, 0, 3.5, 1, 'ralphHeadSphere', self.ralphHeadSphereHandler, True)

        self.ralphGroundRay = CollisionRay()
        self.ralphGroundRay.setOrigin(0,0,1000)
        self.ralphGroundRay.setDirection(0,0,-1)
        self.ralphGroundCol = CollisionNode('ralphRay')
        self.ralphGroundCol.addSolid(self.ralphGroundRay)
        self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol)
        self.ralphGroundRayHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundRayHandler)

        # Enemy collision
        self.goombaSphereHandler = CollisionHandlerQueue()
        self.goombaSphereColNp = self.setupSphereCollider(self.enemies[0][0], 0, 0, 1, 3, 'goombaSphere', self.goombaSphereHandler, False)

        # Uncomment this line to show a visual representation of the 
        # collisions occuring
        self.cTrav.showCollisions(render)
        
        # Create some lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.3, .3, .3, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(-5, -5, -5))
        directionalLight.setColor(Vec4(1, 1, 1, 1))
        directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    # toggle viewing collisions
    def toggleViewCollisions(self):
        if (self.VIEWCOL == 1):
            self.VIEWCOL = 0
            self.ralphGroundSphereColNp.hide()
            self.ralphHeadSphereColNp.hide()
            self.goombaSphereColNp.hide()
            self.ralphGroundColNp.hide()
        else:
            self.VIEWCOL = 1
            self.ralphGroundSphereColNp.show()
            self.ralphHeadSphereColNp.show()
            self.goombaSphereColNp.show()
            self.ralphGroundColNp.show()

    # toggle the camera mode
    def toggleCameraMode(self):
        if (self.CAMSTYLE == 1):
            self.CAMSTYLE = 2
            self.setInst(5, "Camera: third person")
        else:
            self.CAMSTYLE = 1
            self.setInst(5, "Camera: first person")

    def setupSphereCollider(self, target, x, y, z, r, name, handler, isFrom):
        sphere = CollisionSphere(x, y, z, r)
        sphereCol = CollisionNode(name)
        sphereCol.addSolid(sphere)
        if isFrom:
            sphereCol.setFromCollideMask(BitMask32.bit(0))
            sphereCol.setIntoCollideMask(BitMask32.allOff())
        else:
            sphereCol.setFromCollideMask(BitMask32.allOff())
            sphereCol.setIntoCollideMask(BitMask32.bit(0))
        np = target.attachNewNode(sphereCol)
        self.cTrav.addCollider(np, handler)
        return np

    #Records the state of the arrow keys
    def setKey(self, key, value):
        self.keyMap[key] = value
    
    #Task to control looking up and down with the mouse
    def cameraTask(self,task):  
        if (self.CAMSTYLE == 1):
            # first person
            try: 
               # Get mouse coordinates from mouse watcher 
                if ( base.mouseWatcherNode.hasMouse() ):
                    yPos=base.mouseWatcherNode.getMouseY()
                    xPos=base.mouseWatcherNode.getMouseX()
                    camPitch = yPos * self.YROTCONST
                    camYaw = xPos * self.XROTCONST + self.ralph.getH() - 180.0
                    camRoll = base.camera.getR()
                    base.camera.setHpr(camYaw, camPitch, camRoll)
                    base.win.movePointer(0, center[0], center[1]) 
                
            except: 
                # If mouse goes outside window, catch the 
                #    exception here and pass gracefully. 
                pass 
            
            # always put camera where Ralph is
            base.camera.setPos(self.ralph.getX(), self.ralph.getY(), self.ralph.getZ()+4)
        elif (self.CAMSTYLE == 2):
            # third person
            try: 
               # Get mouse coordinates from mouse watcher 
                if ( base.mouseWatcherNode.hasMouse() ):
                    yPos=base.mouseWatcherNode.getMouseY()
                    xPos=base.mouseWatcherNode.getMouseX()
                    camAz = xPos * self.CAM_AZ_SCALE + self.CAM_AZ_OFFSET
                    camZen = yPos * self.CAM_ZEN_SCALE + self.CAM_ZEN_OFFSET
                    self.setInst(3, "cam Az,Zen: {0}, {1}".format(camAz, camZen))
                    camX = self.CAMDISTANCE * math.sin(camZen) * math.cos(camAz)
                    camY = self.CAMDISTANCE * math.sin(camZen) * math.sin(camAz)
                    camZ = self.CAMDISTANCE * math.cos(camZen)
                    self.setInst(4, "cam XYZ: {0}, {1}".format(camX, camY,camZ))
                    base.camera.setPos(self.ralph.getX() + camX, self.ralph.getY() + camY, self.ralph.getZ() + camZ)
                    base.camera.lookAt(self.ralph)
                    base.win.movePointer(0, center[0], center[1]) 
                
            except: 
                # If mouse goes outside window, catch the 
                #    exception here and pass gracefully. 
                pass 
           
        return task.cont

    # Accepts arrow keys to move either the player or the menu cursor,
    # Also deals with grid checking and collision detection
    def move(self, task):

        # Get time delta
        # NOTE: this ends up being equal to globalClock.getDt() but
        # for mysterious unknown reasons if I try to use that in the
        # falling code, everything gets screwed up. -MDD
        curTime = globalClock.getFrameTime()
        self.dTime = curTime - self.lastTime
        self.lastTime = curTime


        # save ralph's initial position so that we can restore it,
        # in case he falls off the map or runs into something.
        startpos = self.ralph.getPos()

        # Are we walking or running?
        if (self.keyMap["running"]!=0):
            movespeed = self.RUNSPEED
        else:
            movespeed = self.WALKSPEED

        # point ralph in the right direction
        if (self.keyMap["turned"]!=0):
            self.ralph.setHpr(0.0, 0, 0)
        else:
            self.ralph.setHpr(180.0, 0, 0)

        # If a move-key is pressed, move ralph in the specified direction.
        if (self.keyMap["left"]!=0):
            self.ralph.setH(self.ralph.getH() + 300 * globalClock.getDt())
        if (self.keyMap["right"]!=0):
            self.ralph.setH(self.ralph.getH() - 300 * globalClock.getDt())
        if (self.keyMap["forward"]!=0):
            self.ralph.setY(self.ralph, -movespeed * globalClock.getDt())
            self.moveClippingPlane()
        if (self.keyMap["backward"]!=0):
            self.ralph.setY(self.ralph, movespeed * globalClock.getDt())
            self.moveClippingPlane()

        # If ralph is moving, loop the run animation.
        # If he is standing still, stop the animation.
        if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0) or (self.keyMap["backward"]!=0):
            if self.isMoving is False:
                self.ralph.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.ralph.stop()
                self.ralph.pose("walk",5)
                self.isMoving = False

        # Jumping
        if (self.keyMap["jumping"]!=0) and (self.keyMap["grounded"]!=0):
            self.setInst(2, "JUMPING: {0}".format(self.keyMap["jumping"]))
            self.upSpeed = self.JUMPVECTOR
            self.setInst(1, "upSpeed: {0}".format(self.upSpeed))
            self.jumpSound.play()

        # Now check for collisions.
        self.cTrav.traverse(render)
  
        # assume Ralph is grounded, unless we find otherwise 
        # in the next bit of code
        self.setKey("grounded", 0)

        # Adjust ralph's Z coordinate.  If ralph's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.
        if (self.keyMap["jumping"]==0):
            entries = []
            for i in range(self.ralphGroundRayHandler.getNumEntries()):
                entry = self.ralphGroundRayHandler.getEntry(i)
                entries.append(entry)
            entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                         x.getSurfacePoint(render).getZ()))
            if (len(entries)>0):
                # find ground under Ralph
                for i in range(self.ralphGroundRayHandler.getNumEntries()):
                    groundDiff = entries[i].getSurfacePoint(render).getZ() - self.ralph.getZ() 
                    if (groundDiff < 1.2) and (groundDiff > -1.2): 
                        # Ralph is close enough to the ground - lock him to it
                        self.ralph.setZ(entries[i].getSurfacePoint(render).getZ())
                        self.upSpeed = 0.0
                        self.setInst(1, "upSpeed: {0}".format(self.upSpeed))
                        self.downForce = 0.0
                        self.setKey("grounded", 1)
                        self.setInst(2, "JUMPING: {0}".format(self.keyMap["jumping"]))
            else:
                self.setInst(0, "Out of bounds!")

        #
        # Sphere ground handler for wall collision
        #
        entries = []
        for i in range(self.ralphGroundSphereHandler.getNumEntries()):
            entry = self.ralphGroundSphereHandler.getEntry(i)
            entries.append(entry)
        if (len(entries)>0): 
            # Ralph has hit an obstacle, don't let him move forward
            self.ralph.setPos(startpos)
            self.setInst(0, "Hit Obstacle")

        #
        # Head sphere handler
        #
        hitHead = 0
        entries = []
        for i in range(self.ralphHeadSphereHandler.getNumEntries()):
            entry = self.ralphHeadSphereHandler.getEntry(i)
            entries.append(entry)
            objY = entries[i].getSurfacePoint(render).getY()
            objZ = entries[i].getSurfacePoint(render).getZ()
            if (self.upSpeed > 0) and (objZ - self.ralph.getZ() > 0.0)\
            and (abs(objY - self.ralph.getY()) < 1.0):
                # the object was above Ralph; halt his upward velocity
                # HIT BLOCK NOISE -- TEMP -- FIXME
                self.coinSound.play()
                self.upSpeed = 0;
                self.setKey("grounded", 0)
                self.setInst(0, "Ungrounded")
                # make sure he doesn't get stuck in the block
                self.ralph.setZ(objZ - 1.5)
                hitHead = 1
                
        if (not hitHead) and (len(entries)>0): 
            # Ralph has hit an obstacle, don't let him move forward
            self.ralph.setPos(startpos)
            self.setInst(0, "Hit Obstacle")

        # gravity
        if (self.keyMap["grounded"] == 0):
            # Ralph is above the ground, make him fall
            self.ralph.setZ(self.ralph.getZ() + (self.upSpeed * self.dTime))
            self.downForce = self.downForce + (self.GRAVITY * self.dTime)
            self.upSpeed = self.upSpeed - self.downForce
            if (self.upSpeed < 0.0): self.setKey("jumping", 0)
            self.setInst(2, "JUMPING: {0}".format(self.keyMap["jumping"]))
            if (self.upSpeed < self.MAXDOWNSPEED): self.upSpeed = self.MAXDOWNSPEED
            self.setInst(1, "upSpeed: {0}".format(self.upSpeed))
            #self.setKey("grounded", 0)
            #self.setInst(0, "Grounded")
        
        # falling to death
        if (self.ralph.getZ() < self.ZBOUND):
            self.die() 
            return task.done
        else:
            return task.cont

    def initClippingPlane(self):

        # Clipping plane variables
        self.X_OFF = 2
        self.Y_OFF = 0
        self.Z_OFF = 10
        
        self.CPYAW = 0
        self.CPPITCH = 0
        self.CPROLL = 0

        self.VIDYAW = 0
        self.VIDPITCH = 0
        self.VIDROLL = 0

        self.CP_SX = 10
        self.CP_SY = 20
        self.CP_SZ = 18
        
        self.clipping_plane = loader.loadModel("../models/clipping-plane/clipping_plane.egg")
        self.clipping_plane.reparentTo(render)
        self.clipping_plane.setPos(self.ralph.getX() + self.X_OFF, self.ralph.getY() + self.Y_OFF, self.ralph.getZ() + self.Z_OFF)
        self.clipping_plane.setHpr(self.CPYAW, self.CPPITCH, self.CPROLL)
        self.clipping_plane.setScale(self.CP_SX, self.CP_SY, self.CP_SZ)

        self.tex0 = MovieTexture("movie0")
        assert self.tex0.read("../models/video/horse_race.avi"), "Video 0 FAIL"
        self.tex1 = MovieTexture("movie1")
        assert self.tex1.read("../models/video/lightning.avi"), "Video 1 FAIL"
        
        self.vid = loader.loadModel("../models/clipping-plane/video_screen.egg")
        self.vid.reparentTo(render)
        self.vid.setTexture(self.tex0, 1)
        self.vid.setPos(self.ralph.getX() + self.X_OFF, self.ralph.getY() + self.Y_OFF, self.ralph.getZ() + self.Z_OFF)
        self.vid.setHpr(self.VIDYAW, self.VIDPITCH, self.VIDROLL)
        self.vid.setScale(self.CP_SX, self.CP_SY, self.CP_SZ)
        
    def moveClippingPlane(self):
        self.clipping_plane.setY(self.ralph.getY() + self.Y_OFF)
        self.vid.setY(self.ralph.getY() + self.Y_OFF)

    def changeVideo(self, vidNum):
        if vidNum == 0:
            self.vid.setTexture(self.tex0, 1)
        elif vidNum == 1:
            self.vid.setTexture(self.tex1, 1)
            
    # Sound tester
    def soundTester(self):
        self.winStageSound.play()

    # Death screen
    def die(self):
        self.deathText.show(BitMask32.allOn())
        self.deathText2.show(BitMask32.allOn())
        self.gameoverSound.play()
        #exit() # for now just end the program

    # Enemy movement
    def goombaTarget(self, enemyRecord, point3):
        direction = enemyRecord[1]
        return ( point3[0], point3[1] + 2*direction, point3[2] )
        
    def nextGoombaMove(self, goombaRecord, moveTarget) :
        newTarget = self.goombaTarget(goombaRecord, moveTarget)
        Sequence(LerpPosInterval(goombaRecord[0], 1, newTarget),
                 Func(self.nextGoombaMove, goombaRecord, newTarget)).start()
        #sequence.append(LerpPosInterval(goombaRecord[0], 3, self.goombaTarget(goombaRecord)))

w = World()
run()

